package both.map.mapModel;

import java.awt.Dimension;
import java.util.List;
import java.util.Vector;
import java.util.ArrayList;

/**
 * the map which is stored in a MapModel. The map is made up of chunks,
 * which have coordinates of their own. 
 * @author jdl
 *
 */
public class ModelMap {
	private final Dimension chunkSize;
	private final Dimension chunkGridSize;
	
	private List<ModelChunk> chunks;
	
	/* each element of chunksIndex is an int giving the index within 'chunks'
	 * of the chunk in that position. E.g. chunksIndex[3][-4] contains '6', meaning
	 * that chunks.get(6) will get the chunk in physical position (3,-4).
	 */
	private int[][] chunksIndex;
	
	/**
	 * build a new map, with a designated centre point.
	 * @param chunkSize the dimensions of each chunk on this map
	 * @param chunkGridSize the dimensions of the grid containing chunks in
	 * this map.
	 */
	public ModelMap(Dimension chunkSize, Dimension chunkGridSize)
	{	
		this.chunkSize = chunkSize;
		this.chunkGridSize = chunkGridSize;
		chunks = new Vector<ModelChunk>();
		
		
		chunksIndex = new int[chunkGridSize.width][chunkGridSize.height];
		
		for (int j=0; j<chunksIndex.length; j++) {
			for (int i=0; i<chunksIndex[j].length; i++) {
				chunksIndex[i][j] = -1;
			}
		}
	}
	
	/**
	 * add an existing chunk to the map in the specified position
	 * @param xPos the x coordinate of the chunk within the map, relative to the map's centrepoint
	 * @param yPos the y coordinate of the chunk within the map, relative to the map's centrepoint
	 * @param newChunk the chunk to be added
	 * @throws IllegalArgumentException if the specified position is out of bounds,
	 * or newChunk's size does not match the chunkSize in this map
	 */
	public void updateChunk(int xPos, int yPos, ModelChunk newChunk)
	{
		if (! newChunk.getSize().equals(getChunksize())) {
			throw new IllegalArgumentException("Update chunk size " + newChunk.getSize() +
							"\nMap chunk size " + getChunksize());
		}
		int index = indexFromPos(xPos, yPos);
		
		if (index == -1) {
			chunks.add(newChunk);
			chunksIndex[xPos][yPos] = chunks.indexOf(newChunk);
		} else {
			chunks.add(index, newChunk);
		}
	}
	
	/**
	 * get the chunk at a specified location
	 * @param xPos
	 * @param yPos
	 * @return the requested chunk (null if there is no chunk there)
	 */
	public ModelChunk getChunkAt(int xPos, int yPos)
	{
		int index = indexFromPos(xPos, yPos);
		
		if (index == -1) {
			return null;
		}
		
		return chunks.get(index);
	}
	
	/**
	 * get the index of a chunk within chunks from its position
	 * @param xPos
	 * @param yPos
	 * @return the relevant index, or -1 if there is no chunk there
	 * @throws IllegalArgumentException if the coordinates are out of bounds
	 */
	private int indexFromPos(int xPos, int yPos)
	{		
		// check if the specified position is out of bounds
		if (xPos < 0 || yPos < 0 || xPos >= chunkGridSize.width || yPos >= chunkGridSize.height) {
			throw new IllegalArgumentException("updateChunk position out of bounds [" + 
													xPos + "," + yPos + "]");			
		}
		int index = chunksIndex[xPos][yPos];
		return index;
	}
	
	/**
	 * @return the dimensions of the chunks in use in this map
	 */
	public Dimension getChunksize()
	{
		return chunkSize;
	}
	
	/**
	 * @return a list of all coordinates which contain an active chunk
	 */
	public List<Dimension> getActiveChunksCoordinates()
	{
		List<Dimension> activeChunks = new ArrayList<Dimension>();
		
		for (int j=0; j<chunksIndex.length; j++) {
			for (int i=0; i<chunksIndex[j].length; i++) {
				if (chunksIndex[i][j] != -1) {
					activeChunks.add(new Dimension(i,j));
				}
			}
		}
		return activeChunks;
	}
	
	/**
	 * get an update of this map
	 */
	public ModelMap grabUpdate()
	{
		ModelMap updateMap = new ModelMap(chunkSize, chunkGridSize);
		List<Dimension> activeChunks = getActiveChunksCoordinates();
		boolean atLeastOneUpdate = false;
		for (Dimension loc : activeChunks) {
			ModelChunk chunk = getChunkAt(loc.width, loc.height);
			ModelChunk chunkUpdate = chunk.grabUpdate();
			if (chunkUpdate != null) {
				updateMap.updateChunk(loc.width, loc.height, chunkUpdate);
				atLeastOneUpdate = true;
			}
		}
		if (atLeastOneUpdate) {
			return updateMap;
		} else {
			return null;
		}
	}
	
	/**
	 * apply an update to this map
	 * @param update the update to be applied to this map
	 */
	public void applyUpdate(ModelMap update)
		throws UpdateStatusException
	{
		if (update != null) {
			if (! update.getChunksize().equals(this.getChunksize())) {
				throw new UpdateStatusException("Chunksize of update is " +
						"different to chunksize of map.");
			}
			
			List<Dimension> updateChunks = update.getActiveChunksCoordinates();
			for (Dimension loc : updateChunks) {
				ModelChunk chunkToBeUpdated = getChunkAt(loc.width, loc.height);
				if (chunkToBeUpdated == null) {
					updateChunk(loc.width, loc.height, new ModelChunk(chunkSize));
				} 
				getChunkAt(loc.width, loc.height).applyUpdate(update.getChunkAt(loc.width, loc.height));
			}
		}
	}
}